Add full-featured form component similar to Airtable#20
Conversation
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
|
@copilot 继续 |
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
已完成!Form 组件已经实现并测试通过。 实现的功能: 测试结果:
提交: b7c1d8f |
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive Airtable-style form component with react-hook-form integration, adding form validation, conditional field rendering, multi-column layouts, and multiple field type support to the Object UI component library.
Changes:
- Added full-featured Form component with react-hook-form integration supporting validation, conditional fields, and multi-column layouts
- Created comprehensive documentation covering usage, field types, validation rules, and accessibility
- Added playground example demonstrating all form features including conditional logic and validation
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 13 comments.
| File | Description |
|---|---|
packages/components/src/renderers/form/form.tsx |
New form renderer component with validation, conditional fields, and multi-column support |
packages/components/src/renderers/form/index.ts |
Added form component to exports |
docs/components/form.md |
Comprehensive documentation for form component features and usage |
apps/playground/src/data/examples.ts |
Added airtable-form example demonstrating all features |
| { name: 'resetOnSubmit', type: 'boolean', label: 'Reset After Submit', defaultValue: false }, | ||
| { name: 'disabled', type: 'boolean', label: 'Disabled', defaultValue: false }, | ||
| { name: 'className', type: 'string', label: 'CSS Class' }, | ||
| { name: 'fieldContainerClass', type: 'string', label: 'Field Container CSS Class' } |
There was a problem hiding this comment.
The onSubmit prop is extracted from the schema but is not documented in the component's metadata inputs array (lines 234-275). This makes it invisible to the designer tool and unclear to users how to configure form submission behavior.
While the documentation mentions using onAction for form submission (which is the recommended approach), if onSubmit is a supported schema property, it should be included in the inputs array with appropriate type and description.
| { name: 'fieldContainerClass', type: 'string', label: 'Field Container CSS Class' } | |
| { name: 'fieldContainerClass', type: 'string', label: 'Field Container CSS Class' }, | |
| { | |
| name: 'onSubmit', | |
| type: 'object', | |
| label: 'On Submit (advanced)', | |
| description: | |
| 'Optional submit handler configuration. Prefer using onAction with a "submit" action for most use cases.' | |
| } |
| import { ComponentRegistry } from '@object-ui/core'; | ||
| import { useForm } from 'react-hook-form'; | ||
| import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from '@/ui/form'; | ||
| import { Button } from '@/ui/button'; | ||
| import { Input } from '@/ui/input'; | ||
| import { Textarea } from '@/ui/textarea'; | ||
| import { Checkbox } from '@/ui/checkbox'; | ||
| import { | ||
| Select, | ||
| SelectTrigger, | ||
| SelectValue, | ||
| SelectContent, | ||
| SelectItem | ||
| } from '@/ui/select'; | ||
| import { renderChildren } from '../../lib/utils'; | ||
| import { Alert, AlertDescription } from '@/ui/alert'; | ||
| import { AlertCircle, Loader2 } from 'lucide-react'; | ||
| import React from 'react'; | ||
|
|
||
| // Form renderer component - Airtable-style feature-complete form | ||
| ComponentRegistry.register('form', | ||
| ({ schema, className, onAction, ...props }) => { | ||
| const { | ||
| defaultValues = {}, | ||
| fields = [], | ||
| submitLabel = 'Submit', | ||
| cancelLabel = 'Cancel', | ||
| showCancel = false, | ||
| layout = 'vertical', | ||
| columns = 1, | ||
| onSubmit: onSubmitProp, | ||
| onChange: onChangeProp, | ||
| resetOnSubmit = false, | ||
| validationMode = 'onSubmit', | ||
| disabled = false, | ||
| } = schema; | ||
|
|
||
| // Initialize react-hook-form | ||
| const form = useForm({ | ||
| defaultValues, | ||
| mode: validationMode, | ||
| }); | ||
|
|
||
| const [isSubmitting, setIsSubmitting] = React.useState(false); | ||
| const [submitError, setSubmitError] = React.useState<string | null>(null); | ||
|
|
||
| // Watch for form changes | ||
| React.useEffect(() => { | ||
| if (onChangeProp && onAction) { | ||
| const subscription = form.watch((data) => { | ||
| onAction({ | ||
| type: 'form_change', | ||
| data, | ||
| formData: data, | ||
| }); | ||
| }); | ||
| return () => subscription.unsubscribe(); | ||
| } | ||
| }, [form, onAction, onChangeProp]); | ||
|
|
||
| // Handle form submission | ||
| const handleSubmit = form.handleSubmit(async (data) => { | ||
| setIsSubmitting(true); | ||
| setSubmitError(null); | ||
|
|
||
| try { | ||
| if (onAction) { | ||
| const result = await onAction({ | ||
| type: 'form_submit', | ||
| data, | ||
| formData: data, | ||
| }); | ||
|
|
||
| // Check if submission returned an error | ||
| if (result?.error) { | ||
| setSubmitError(result.error); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| if (onSubmitProp && typeof onSubmitProp === 'function') { | ||
| await onSubmitProp(data); | ||
| } | ||
|
|
||
| if (resetOnSubmit) { | ||
| form.reset(); | ||
| } | ||
| } catch (error: any) { | ||
| setSubmitError(error?.message || 'An error occurred during submission'); | ||
| console.error('Form submission error:', error); | ||
| } finally { | ||
| setIsSubmitting(false); | ||
| } | ||
| }); | ||
|
|
||
| // Handle cancel | ||
| const handleCancel = () => { | ||
| form.reset(); | ||
| if (onAction) { | ||
| onAction({ | ||
| type: 'form_cancel', | ||
| data: form.getValues(), | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| // Determine grid classes based on columns | ||
| const gridClass = columns > 1 | ||
| ? `grid gap-4 md:grid-cols-${Math.min(columns, 4)}` | ||
| : 'space-y-4'; | ||
|
|
||
| return ( | ||
| <Form {...form}> | ||
| <form onSubmit={handleSubmit} className={className} {...props}> | ||
| {/* Form Error Alert */} | ||
| {submitError && ( | ||
| <Alert variant="destructive" className="mb-4"> | ||
| <AlertCircle className="h-4 w-4" /> | ||
| <AlertDescription>{submitError}</AlertDescription> | ||
| </Alert> | ||
| )} | ||
|
|
||
| {/* Form Fields */} | ||
| {schema.children ? ( | ||
| // If children are provided directly, render them | ||
| <div className={schema.fieldContainerClass || 'space-y-4'}> | ||
| {renderChildren(schema.children)} | ||
| </div> | ||
| ) : ( | ||
| // Otherwise render fields from schema | ||
| <div className={schema.fieldContainerClass || gridClass}> | ||
| {fields.map((field: any, index: number) => { | ||
| const { | ||
| name, | ||
| label, | ||
| description, | ||
| type = 'input', | ||
| required = false, | ||
| disabled: fieldDisabled = false, | ||
| validation = {}, | ||
| condition, | ||
| ...fieldProps | ||
| } = field; | ||
|
|
||
| // Handle conditional rendering | ||
| if (condition) { | ||
| const watchField = condition.field; | ||
| const watchValue = form.watch(watchField); | ||
|
|
||
| if (condition.equals && watchValue !== condition.equals) { | ||
| return null; | ||
| } | ||
| if (condition.notEquals && watchValue === condition.notEquals) { | ||
| return null; | ||
| } | ||
| if (condition.in && !condition.in.includes(watchValue)) { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| // Build validation rules | ||
| const rules: any = { | ||
| ...validation, | ||
| }; | ||
|
|
||
| if (required) { | ||
| rules.required = validation.required || `${label || name} is required`; | ||
| } | ||
|
|
||
| return ( | ||
| <FormField | ||
| key={field.id || name || index} | ||
| control={form.control} | ||
| name={name} | ||
| rules={rules} | ||
| render={({ field: formField }) => ( | ||
| <FormItem> | ||
| {label && ( | ||
| <FormLabel> | ||
| {label} | ||
| {required && <span className="text-destructive ml-1">*</span>} | ||
| </FormLabel> | ||
| )} | ||
| <FormControl> | ||
| {/* Render the actual field component based on type */} | ||
| {renderFieldComponent(type, { | ||
| ...fieldProps, | ||
| ...formField, | ||
| inputType: fieldProps.inputType, | ||
| options: fieldProps.options, | ||
| disabled: disabled || fieldDisabled || isSubmitting, | ||
| })} | ||
| </FormControl> | ||
| {description && ( | ||
| <FormDescription>{description}</FormDescription> | ||
| )} | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| ); | ||
| })} | ||
| </div> | ||
| )} | ||
|
|
||
| {/* Form Actions */} | ||
| {(schema.showActions !== false) && ( | ||
| <div className={`flex gap-2 ${layout === 'horizontal' ? 'justify-end' : 'justify-start'} mt-6`}> | ||
| {showCancel && ( | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| onClick={handleCancel} | ||
| disabled={isSubmitting || disabled} | ||
| > | ||
| {cancelLabel} | ||
| </Button> | ||
| )} | ||
| <Button | ||
| type="submit" | ||
| disabled={isSubmitting || disabled} | ||
| > | ||
| {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} | ||
| {submitLabel} | ||
| </Button> | ||
| </div> | ||
| )} | ||
| </form> | ||
| </Form> | ||
| ); | ||
| }, | ||
| { | ||
| label: 'Form', | ||
| inputs: [ | ||
| { | ||
| name: 'fields', | ||
| type: 'array', | ||
| label: 'Fields', | ||
| description: 'Array of field configurations with name, label, type, validation, etc.' | ||
| }, | ||
| { | ||
| name: 'defaultValues', | ||
| type: 'object', | ||
| label: 'Default Values', | ||
| description: 'Object with default values for form fields' | ||
| }, | ||
| { name: 'submitLabel', type: 'string', label: 'Submit Button Label', defaultValue: 'Submit' }, | ||
| { name: 'cancelLabel', type: 'string', label: 'Cancel Button Label', defaultValue: 'Cancel' }, | ||
| { name: 'showCancel', type: 'boolean', label: 'Show Cancel Button', defaultValue: false }, | ||
| { | ||
| name: 'layout', | ||
| type: 'enum', | ||
| enum: ['vertical', 'horizontal'], | ||
| label: 'Layout', | ||
| defaultValue: 'vertical' | ||
| }, | ||
| { | ||
| name: 'columns', | ||
| type: 'number', | ||
| label: 'Number of Columns', | ||
| defaultValue: 1, | ||
| description: 'For multi-column layouts (1-4)' | ||
| }, | ||
| { | ||
| name: 'validationMode', | ||
| type: 'enum', | ||
| enum: ['onSubmit', 'onBlur', 'onChange', 'onTouched', 'all'], | ||
| label: 'Validation Mode', | ||
| defaultValue: 'onSubmit' | ||
| }, | ||
| { name: 'resetOnSubmit', type: 'boolean', label: 'Reset After Submit', defaultValue: false }, | ||
| { name: 'disabled', type: 'boolean', label: 'Disabled', defaultValue: false }, | ||
| { name: 'className', type: 'string', label: 'CSS Class' }, | ||
| { name: 'fieldContainerClass', type: 'string', label: 'Field Container CSS Class' } | ||
| ], | ||
| defaultProps: { | ||
| submitLabel: 'Submit', | ||
| cancelLabel: 'Cancel', | ||
| showCancel: false, | ||
| layout: 'vertical', | ||
| columns: 1, | ||
| validationMode: 'onSubmit', | ||
| resetOnSubmit: false, | ||
| disabled: false, | ||
| fields: [ | ||
| { | ||
| name: 'name', | ||
| label: 'Name', | ||
| type: 'input', | ||
| required: true, | ||
| placeholder: 'Enter your name', | ||
| }, | ||
| { | ||
| name: 'email', | ||
| label: 'Email', | ||
| type: 'input', | ||
| inputType: 'email', | ||
| required: true, | ||
| placeholder: 'Enter your email', | ||
| }, | ||
| ], | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| // Helper function to render field components | ||
| function renderFieldComponent(type: string, props: any) { | ||
| const { schema, inputType, options, ...fieldProps } = props; | ||
|
|
||
| switch (type) { | ||
| case 'input': | ||
| return <Input type={inputType || 'text'} {...fieldProps} />; | ||
|
|
||
| case 'textarea': | ||
| return <Textarea {...fieldProps} />; | ||
|
|
||
| case 'checkbox': | ||
| // For checkbox, we need to handle the value differently | ||
| const { value, onChange, ...checkboxProps } = fieldProps; | ||
| return ( | ||
| <div className="flex items-center space-x-2"> | ||
| <Checkbox | ||
| checked={value} | ||
| onCheckedChange={onChange} | ||
| {...checkboxProps} | ||
| /> | ||
| </div> | ||
| ); | ||
|
|
||
| case 'select': | ||
| // For select with react-hook-form, we need to handle the onChange | ||
| const { value: selectValue, onChange: selectOnChange, ...selectProps } = fieldProps; | ||
| return ( | ||
| <Select value={selectValue} onValueChange={selectOnChange} {...selectProps}> | ||
| <SelectTrigger> | ||
| <SelectValue placeholder="Select an option" /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| {options?.map((opt: any) => ( | ||
| <SelectItem key={opt.value} value={opt.value}> | ||
| {opt.label} | ||
| </SelectItem> | ||
| ))} | ||
| </SelectContent> | ||
| </Select> | ||
| ); | ||
|
|
||
| default: | ||
| return <Input type={inputType || 'text'} {...fieldProps} />; | ||
| } | ||
| } |
There was a problem hiding this comment.
Multiple uses of any type reduce type safety. Consider defining proper TypeScript interfaces for:
- Field configuration (line 132:
field: any) - Render props (line 307:
props: any) - Select options (line 339:
opt: any) - Error object (line 88:
error: any)
Creating proper type definitions would improve IDE autocomplete, catch potential bugs at compile time, and align with the TypeScript 5.0+ (Strict) requirement in the coding guidelines.
|
|
||
| return ( | ||
| <FormField | ||
| key={field.id || name || index} |
There was a problem hiding this comment.
The key generation uses field.id || name || index which could lead to issues if field names change between renders or if the same name is used multiple times. Using index as a fallback can cause React reconciliation issues when fields are reordered.
Consider requiring field.id or field.name to always be present, or generate a stable unique ID during form initialization. React documentation recommends avoiding using array indices as keys when the list can change.
| key={field.id || name || index} | |
| key={field.id ?? name} |
| React.useEffect(() => { | ||
| if (onChangeProp && onAction) { | ||
| const subscription = form.watch((data) => { | ||
| onAction({ | ||
| type: 'form_change', | ||
| data, | ||
| formData: data, | ||
| }); | ||
| }); | ||
| return () => subscription.unsubscribe(); | ||
| } | ||
| }, [form, onAction, onChangeProp]); |
There was a problem hiding this comment.
The onChange prop check in the useEffect dependency array could be problematic. The condition checks onChangeProp && onAction, but onChangeProp is extracted from the schema which might be a boolean or a function. If it's a function, it's not being called, only used as a flag to enable change tracking via onAction.
This could be confusing. Consider either:
- Using
onChangePropas a callback directly if it's a function - Documenting that
onChangeshould be a boolean flag, not a function - Removing
onChangePropentirely and always tracking changes whenonActionexists
| import { ComponentRegistry } from '@object-ui/core'; | ||
| import { useForm } from 'react-hook-form'; | ||
| import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from '@/ui/form'; | ||
| import { Button } from '@/ui/button'; | ||
| import { Input } from '@/ui/input'; | ||
| import { Textarea } from '@/ui/textarea'; | ||
| import { Checkbox } from '@/ui/checkbox'; | ||
| import { | ||
| Select, | ||
| SelectTrigger, | ||
| SelectValue, | ||
| SelectContent, | ||
| SelectItem | ||
| } from '@/ui/select'; | ||
| import { renderChildren } from '../../lib/utils'; | ||
| import { Alert, AlertDescription } from '@/ui/alert'; | ||
| import { AlertCircle, Loader2 } from 'lucide-react'; | ||
| import React from 'react'; |
There was a problem hiding this comment.
The cn utility function from @/lib/utils should be imported and used for className construction to ensure proper class merging with Tailwind. This is the standard pattern used throughout the codebase (see grid.tsx, card.tsx, etc.) and follows the Shadcn/Tailwind best practice mentioned in the coding guidelines.
Import cn and use it to construct className values, especially when combining multiple conditional classes.
| "pattern": { | ||
| "value": "^[a-zA-Z0-9\\s]+$", | ||
| "message": "Only letters, numbers, and spaces allowed" |
There was a problem hiding this comment.
The validation pattern value is provided as a string in the documentation (line 99), but react-hook-form expects a RegExp object. While react-hook-form might convert strings to RegExp internally, this behavior is not guaranteed and could lead to validation failures.
The documentation should clarify whether pattern values should be regex strings or RegExp objects, and the implementation should handle both cases or provide clear error messages if the wrong type is provided.
| const gridClass = columns > 1 | ||
| ? `grid gap-4 md:grid-cols-${Math.min(columns, 4)}` |
There was a problem hiding this comment.
Dynamic Tailwind classes constructed with template literals won't work correctly because Tailwind's JIT compiler cannot detect them at build time. The class md:grid-cols-${Math.min(columns, 4)} will not be included in the compiled CSS.
To fix this, use conditional logic similar to the grid component pattern (see packages/components/src/renderers/layout/grid.tsx lines 14-25), where each possible class is explicitly listed:
Instead of template literal interpolation, use:
- columns === 1 && 'md:grid-cols-1'
- columns === 2 && 'md:grid-cols-2'
- columns === 3 && 'md:grid-cols-3'
- columns >= 4 && 'md:grid-cols-4'
This ensures the Tailwind compiler can detect and include all necessary classes at build time.
| const gridClass = columns > 1 | |
| ? `grid gap-4 md:grid-cols-${Math.min(columns, 4)}` | |
| const gridColsClass = | |
| columns === 1 | |
| ? 'md:grid-cols-1' | |
| : columns === 2 | |
| ? 'md:grid-cols-2' | |
| : columns === 3 | |
| ? 'md:grid-cols-3' | |
| : 'md:grid-cols-4'; | |
| const gridClass = columns > 1 | |
| ? `grid gap-4 ${gridColsClass}` |
| if (condition.notEquals && watchValue === condition.notEquals) { | ||
| return null; | ||
| } | ||
| if (condition.in && !condition.in.includes(watchValue)) { |
There was a problem hiding this comment.
The conditional field logic doesn't handle the case when the watched field value is undefined or null. This could cause unexpected behavior during initial form render or when fields are cleared.
For example, if watchValue is undefined, the condition condition.in && !condition.in.includes(watchValue) might not behave as expected. Consider adding explicit checks for undefined and null values to make the conditional logic more robust.
| if (condition.in && !condition.in.includes(watchValue)) { | |
| if ( | |
| condition.in && | |
| watchValue !== undefined && | |
| watchValue !== null && | |
| !condition.in.includes(watchValue) | |
| ) { |
| {label && ( | ||
| <FormLabel> | ||
| {label} | ||
| {required && <span className="text-destructive ml-1">*</span>} |
There was a problem hiding this comment.
The required field asterisk has a potential accessibility issue. The asterisk character alone may not be announced properly by screen readers. Consider adding aria-label or using visually hidden text to indicate required fields for screen reader users.
For example: <span className="text-destructive ml-1" aria-label="required">*</span> or include "(required)" as visually hidden text.
| {required && <span className="text-destructive ml-1">*</span>} | |
| {required && ( | |
| <span className="text-destructive ml-1" aria-label="required"> | |
| * | |
| </span> | |
| )} |
| {options?.map((opt: any) => ( | ||
| <SelectItem key={opt.value} value={opt.value}> | ||
| {opt.label} | ||
| </SelectItem> | ||
| ))} |
There was a problem hiding this comment.
The Select component doesn't handle the case where options might be undefined or empty. This could cause a runtime error when trying to map over undefined options.
Add a safety check: {options?.map((opt: any) => ... or validate that options exist before rendering the Select component.
|
@copilot 修正你检查出的问题,从主分支合并最新代码,并做完整测试。 |
…lot/add-full-form-component
…mprove error handling and validation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…s check Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
已完成所有代码审查问题的修复、主分支代码合并和全面测试! 修复的问题✅ TypeScript 类型安全
✅ Tailwind CSS 兼容性
✅ 错误处理改进
✅ 条件字段逻辑增强
✅ React Key 管理
✅ 无障碍性改进
✅ Select 组件增强
✅ 文档更新
测试结果所有功能已完整测试:
截图 |
|
✅ All checks passed!
|
|
@copilot :24:46.3447597Z env: |
|
@copilot 2026-01-14T02:35:34.9166638Z ##[group]Run pnpm test |
|
✅ All checks passed!
|
@copilot 修改这个问题 |
太好了!CI 测试全部通过 ✅ 所有测试都成功运行:
pretest 脚本修复生效,Form 组件的 PR 现在已准备好合并。 |
… update input types for consistency
Added .npmrc with auto-install-peers set to true and updated pnpm-lock.yaml to reflect this change. This ensures peer dependencies are automatically installed, improving dependency management and reducing manual intervention.
|
✅ All checks passed!
|
…iguration, and improve Playground component logic
|
✅ All checks passed!
|
…ppy-dom in vite.config.ts
|
✅ All checks passed!
|
… use happy-dom environment
|
✅ All checks passed!
|
|
✅ All checks passed!
|
- #39 showRecordCount: conditionally show/hide record count bar - #24 rowHeight: add short and extra_tall mapping in ListView + bridge - #7 sort: parse legacy string format "field desc" - #22 description: render view description below toolbar - #40 allowPrinting: add print button with window.print() - #31 virtualScroll: forward flag to grid view schema - #35 userActions: wire sort/search/filter/rowHeight to toolbar visibility - #38 addRecord: render "+ Add Record" button from spec config - #37 tabs: render tab bar UI for view tabs - #9 filterableFields: restrict FilterBuilder to whitelist fields - #8 searchableFields: scope search queries to specified fields - #36 appearance: wire showDescription and allowedVisualizations - #16 pageSizeOptions: add page size selector UI in status bar - #17-21: use spec kanban/calendar/gantt/gallery/timeline configs - #20 gallery: add typed GalleryConfig to ListViewSchema - #21 timeline: add typed TimelineConfig to ListViewSchema - Bridge: add short/extra_tall density mapping, filterableFields pass-through Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>



Implement Airtable-like Feature-Complete Form Component
This PR implements a comprehensive Form component with react-hook-form integration, providing an Airtable-style form experience with validation, conditional fields, and multi-column layouts.
Screenshots
Form with all features:
Conditional fields working (Team Size appears when Project Type = "Team"):
Key Features
✅ React Hook Form Integration - Robust form state management and validation
✅ Field Validation - Custom validation rules with error messages (minLength, maxLength, pattern, min, max, required)
✅ Conditional Fields - Dynamic field visibility based on other field values using
equals,notEquals, andinconditions✅ Multi-Column Layout - Responsive 1-4 column grid layouts
✅ Multiple Field Types - Input, textarea, select, checkbox support
✅ Loading States - Visual feedback during form submission
✅ Error Handling - Display both validation and submission errors
✅ Default Values - Pre-populate form fields
✅ Reset Functionality - Clear form or reset to defaults
Recent Fixes
✅ CI Test Failure Fixed (Latest Commit)
@object-ui/corepackage wasn't built before running component testspretestscript to build core and react packages before running component testspackages/components/package.json- Added pretest scriptpackages/components/vite.config.ts- Added test configuration✅ Code Review Fixes (Previous Commits)
TypeScript Type Safety
FormFieldConfig,FieldValidation,FieldCondition,SelectOption,RenderFieldPropsanytypes for better type safety and IDE autocompleteTailwind CSS Compatibility
md:grid-cols-${columns}to conditional logicmd:grid-cols-2,md:grid-cols-3,md:grid-cols-4cn()utility for proper class mergingImproved Error Handling
instanceof ErrorEnhanced Conditional Field Logic
Better Key Management
field.id ?? name(never index alone)Accessibility Improvements
aria-label="required"to required field asterisksSelect Component Enhancements
SelectOptioninterfaceDocumentation Updates
Implementation Details
packages/components/src/renderers/form/form.tsxdocs/components/form.mdairtable-formexample in playgroundTesting
All features have been comprehensively tested:
Example Usage
{ "type": "form", "submitLabel": "Create Project", "columns": 2, "validationMode": "onBlur", "fields": [ { "name": "projectName", "label": "Project Name", "type": "input", "required": true, "placeholder": "Enter project name", "validation": { "minLength": { "value": 3, "message": "Project name must be at least 3 characters" } } }, { "name": "teamSize", "label": "Team Size", "type": "input", "inputType": "number", "placeholder": "Number of team members", "condition": { "field": "projectType", "in": ["team", "enterprise"] } } ] }The Form component is production-ready and integrates seamlessly with the existing Object UI architecture.
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.